{*******************************************************************************
* The contents of this file are used with permission, subject to the Mozilla   *
* Public License Version 1.1 (the "License"); you may not use this file except *
* in compliance with the License. You may obtain a copy of the License at      *
* http://www.mozilla.org/MPL/                                                  *
*                                                                              *
* Software distributed under the License is distributed on an "AS IS" basis,   *
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for *
* the specific language governing rights and limitations under the License.    *
*                                                                              *
* The Original Code is distributed as part of the "mediate" product and is     *
* Copyright (C) @home media limited. All Rights Reserved.                      *
*                                                                              *
* Email: support@athomemedia.co.uk                                             *
* Web:   http://www.athomemedia.co.uk                                          *
*******************************************************************************}

unit Visualisation;

interface

uses Types, AHMTypes, AVRenderer, Canvas, WMPEffects, DirectXSprite,
     VisWaveform, DSPConst, Colour;

const
  VISUALISATION_WIDTHS: array[0..5] of Integer = (160, 320, 480, 640, 800, 1024);
  VISUALISATION_HEIGHTS: array[0..5] of Integer = (120, 240, 360, 480, 600, 768);

  VOLUME_SCALE = 100; // Scale factor to convert 0..-5000 DirectShow range to our 0..-50 range
  VOLUME_OFFSET = 0;
  POSITION_SEEK_DELAY = 500; // Minimum seek delay period in milliseconds

type
  TAHMVisualisationRenderer = class(TAHMAVRenderer)
  private
    FWaveform: TDCWaveform;
    FVisualisation: IWMPEffects;
    FVisualisationUnavailable: Boolean;
    FCache: TAHMDXSprite;
    FLevels: TimedLevel;
    FQuality: Integer;
    procedure ProcessWaveformData(Sender: TObject; Data: PVisualBuffer;
      MinY, MaxY, NumSamples, Channels: Integer);
  protected
    function GetDuration: LongInt; override;
    function GetPosition: LongInt; override;
    procedure SetPosition(Position: LongInt); override;
    procedure SetVolume(Volume: Integer); override;
    function ConvertVolume(Volume: Integer): Integer;
    procedure InitialiseTexture;
    procedure InitialiseVisualisation;
    procedure UpdateVisualisation;
  public
    constructor Create;
    destructor Destroy; override;
    procedure UpdateMediaState; override;
    procedure UpdateRendererSettings; override;
    procedure ReleaseResources; override;
    procedure Render(Rect: TAHMRectF; Shade: TAHMCanvasShade; Gamma: TAHMCanvasColour;
                     XScale, YScale: Single; XFlip, YFlip: Boolean); override;
    function Play: Boolean; override;
    function Pause: Boolean; override;
    function Stop: Boolean; override;
    function Mute: Boolean; override;
    function UnMute: Boolean; override;
    function FastForward: Boolean; override;
    function Rewind: Boolean; override;
    function SkipForward: Boolean; override;
    function SkipBackward: Boolean; override;
  end;

implementation

uses SysUtils, Windows, Math, ComObj, ApplicationManager, DirectXHelper,
     DirectShowManager, Logging, Timing;

constructor TAHMVisualisationRenderer.Create;
begin
  inherited Create;

  // Create and configure Waveform analysis component for visualisation
  FWaveform := TDCWaveform.Create(nil);
  FWaveform.MaxY := 255;
  FWaveform.NumSamples := SA_BUFFER_SIZE;
  FWaveform.MixChannels := False;
  FWaveform.OnWaveformData := ProcessWaveformData;

  // Set position seeking interval for audio source
  FPositionInterval := POSITION_SEEK_DELAY;
end;

destructor TAHMVisualisationRenderer.Destroy;
begin
  FWaveform.Free;

  inherited Destroy;
end;

function TAHMVisualisationRenderer.ConvertVolume(Volume: Integer): Integer;
begin
  // Convert our -40..0 dB range into 0..100 range DirectShow uses
  Result := Floor((Volume + VOLUME_OFFSET) * VOLUME_SCALE);
end;

function TAHMVisualisationRenderer.GetDuration: LongInt;
begin
  with DirectShowInstance do
    if Assigned(DirectShowAudioRenderer) then
      Result := DirectShowAudioRenderer.Duration
    else
      Result := 0;
end;

function TAHMVisualisationRenderer.GetPosition: LongInt;
begin
  with DirectShowInstance do
    if Assigned(DirectShowAudioRenderer) then
      Result := DirectShowAudioRenderer.Position
    else
      Result := 0;
end;

procedure TAHMVisualisationRenderer.SetPosition(Position: LongInt);
begin
  // Okay to update media position?
  if OkayToPositionMedia then
    with DirectShowInstance do
      if Assigned(DirectShowAudioRenderer) then
        DirectShowAudioRenderer.Seek(Position);
end;

procedure TAHMVisualisationRenderer.SetVolume(Volume: Integer);
begin
  inherited SetVolume(Volume);

  with DirectShowInstance do
    if Assigned(DirectShowAudioRenderer) then
      DirectShowAudioRenderer.InternalVolume := ConvertVolume(Volume);
end;

procedure TAHMVisualisationRenderer.InitialiseTexture;
var
  Width, Height: Integer;
begin
  // Only create texture once
  if (FCache <> nil) then Exit;

  // Check preferences for appropriate visualisation texture size
  FQuality := ApplicationManagerInstance.PreferencesManager.VisualisationQuality;
  if (FQuality < 0) or (FQuality > 5) then FQuality := 0;
  Width := VISUALISATION_WIDTHS[FQuality];
  Height := VISUALISATION_HEIGHTS[FQuality];

  // Create appropriate texture for rendering
  FCache := DirectXInstance.SpriteManager.CreateSprite(Width, Height);
  FCache.AlphaBlending := False; // Required so we can lock surface
end;

procedure TAHMVisualisationRenderer.InitialiseVisualisation;
const
  // CLSID for "Alchemy" Windows media player visualisation COM object
  Visual_CLSID: TGUID = '{0AA02E8D-F851-4CB0-9F64-BBA9BE7A983D}';
  Goom_CLSID: TGUID = '{7FB74109-27FC-4007-9AA3-3279420E34F5}'; // new goom!
var
  VisComObj: IUnknown;
begin
  // Only create visualisation once
  if (FVisualisationUnavailable) or (FVisualisation <> nil) then Exit;

  try
    // Try to create goom visualisation COM object first
    VisComObj := CreateComObject(Goom_CLSID);
    if VisComObj <> nil then
      FVisualisation := VisComObj as IWMPEffects;
    FVisualisation.SetCurrentPreset(FQuality);
  except
    LogWarn('Goom visualisation is not available - please check mediate installation');
    FVisualisation := nil;
    try
      // Goom isn't registered, so try to create WMP visualisation instead
      VisComObj := CreateComObject(Visual_CLSID);
      if VisComObj <> nil then
        FVisualisation := VisComObj as IWMPEffects;
    except
      LogError('Alchemy visualisation is not available - please check WMP installation');
      FVisualisation := nil;
      FVisualisationUnavailable := True;
    end;
  end;
end;

procedure TAHMVisualisationRenderer.UpdateMediaState;
begin
  with DirectShowInstance do
    if Assigned(DirectShowAudioRenderer) then
    begin
      // Check for any outstanding direct show events
      DirectShowAudioRenderer.CheckEventQueue;

      // Only really interested in end of file event - signal next item
      if DirectShowAudioRenderer.EndOfFile then SignalMediaEvent(msigNext)
      else if DirectShowAudioRenderer.PlaybackError then SignalMediaEvent(msigError);
    end;
end;

procedure TAHMVisualisationRenderer.UpdateRendererSettings;
begin
  with DirectShowInstance do
    if Assigned(DirectShowAudioRenderer) then
    begin
      DirectShowAudioRenderer.UpdateAudioDSPSettings;
    end;
end;

procedure TAHMVisualisationRenderer.ReleaseResources;
begin
  // Release any resources currently held open
  FVisualisation := nil;
  FVisualisationUnavailable := False;
  FreeAndNil(FCache);
end;

procedure TAHMVisualisationRenderer.UpdateVisualisation;
var
  RenderRect: TRect;
  i, j: Integer;
begin
  // Try to populate our buffer with actual sound data - this will fire our
  // ProcessWaveformData event then continue below if successful
  with DirectShowInstance do
    if (DirectShowAudioRenderer = nil) or (not DirectShowAudioRenderer.SampleAudio(FWaveform)) then
      // Can't obtain real data so we must fill levels with random data
      for i := 0 to 1 do
        for j := 0 to SA_BUFFER_SIZE - 1 do
        begin
          FLevels.frequency[i, j] := 0; // frequency mostly not used & FFT is expensive
          FLevels.waveform[i, j] := Random(255);
        end;

  // Update levels timestamp & state
  FLevels.timeStamp := TimerInstance.CurrentTimeStamp;
  case MediaState of
    msStopped: FLevels.state := 0; // stop_state
    msPaused: FLevels.state := 1; // pause_state
    else FLevels.state := 2; // play_state
  end;

  RenderRect := GetNativeRect(FCache.Bounds);

  // Render visualisation onto our GDI DrawBuffer
  with FCache do
  begin
    if AllocateHandle then
    begin
      try
        FVisualisation.Render(FLevels, SurfaceHandle, RenderRect);
      except
        // Catch and log any problems with Visualisation COM object
        on E:Exception do
          LogWarn('Visualisation COM Object raised exception: ' + E.Message);
      end;
      ReleaseHandle;
    end;
  end;
end;

procedure TAHMVisualisationRenderer.Render(Rect: TAHMRectF; Shade: TAHMCanvasShade;
                                           Gamma: TAHMCanvasColour; XScale, YScale: Single;
                                           XFlip, YFlip: Boolean);
begin
  inherited;

  if MediaState = msStopped then Exit;

  // Perform initialisation tasks
  InitialiseTexture;
  InitialiseVisualisation;
  if FVisualisation = nil then Exit;

  // If we haven't yet updated visualisation this frame then do it
  if UpdateRequired then UpdateVisualisation;

  // Now we can draw our visualisation sprite on screen
  DirectXInstance.DrawSprite(FCache, FCache.SourceRect, Rect, Shade, Gamma, XScale, YScale, XFlip, YFlip);
end;

procedure TAHMVisualisationRenderer.ProcessWaveformData(Sender: TObject; Data: PVisualBuffer;
                                                        MinY, MaxY, NumSamples, Channels: Integer);
var
  i: Integer;
begin
  // Populate our levels buffer with proper sound data from DSP filter
  // ProcessWaveformData event then continue below if successful
  for i := 0 to SA_BUFFER_SIZE - 1 do
  begin
    FLevels.frequency[0, i] := 0; // frequency mostly not used & FFT is expensive
    FLevels.frequency[1, i] := 0; // same for both channels

    // Populate left channel
    FLevels.waveform[0, i] := Data[0, i];

    // Do we have stereo channels? if not duplicate left for right
    if Channels > 1 then FLevels.waveform[1, i] := Data[1, i]
                    else FLevels.waveform[1, i] := Data[0, i];
  end;
end;

function TAHMVisualisationRenderer.Play: Boolean;
begin
  // Set default result - couldn't play
  Result := False;

  if Assigned(Playlist.CurrentItem) then
  begin
    with DirectShowInstance do
    begin
      // Check if audio renderer is currently in use
      if Assigned(DirectShowAudioRenderer) then
      begin
        // In use - if we own it then just resume playback
        if DirectShowAudioRenderer.Owner = Self then
        begin
          DirectShowAudioRenderer.Play;
          if not Muted then DirectShowAudioRenderer.InternalUnMute;
        end
        else
        begin
          // Owned by other stream, so nick it, but inform other stream first
          TAHMAVRenderer(DirectShowAudioRenderer.Owner).SignalMediaEvent(msigAbort);
          StopAudio;
        end;
      end;

      // Check if audio renderer needs initialising
      if not Assigned(DirectShowAudioRenderer) then
      begin
        if not PlayAudio(Playlist.CurrentItem.URL) then
        begin
          // Map DirectX audio error into one of our generic error types
          case DirectShowInstance.LastAudioError of
            daOkay:       LastError := meNone;
            daMissingWMP: LastError := meInstallationBroken;
            daUnknown:    LastError := meUnsupportedMedia;
          end;
          Exit;
        end;

        DirectShowAudioRenderer.InternalVolume := ConvertVolume(Volume);
        if Muted then DirectShowAudioRenderer.InternalFastMute;
        DirectShowAudioRenderer.Owner := Self;
      end;
      Result := True;
    end;
  end;
end;

function TAHMVisualisationRenderer.Pause: Boolean;
begin
  // Set default result - couldn't pause
  Result := False;

  with DirectShowInstance do
    if Assigned(DirectShowAudioRenderer) then
    begin
      if not Muted then DirectShowAudioRenderer.InternalMute;
      DirectShowAudioRenderer.Pause;
      Result := True;
    end;
end;

function TAHMVisualisationRenderer.Stop: Boolean;
begin
  // Stop audio stream
  with DirectShowInstance do
  begin
    if Assigned(DirectShowAudioRenderer) then
      if not Muted then DirectShowAudioRenderer.InternalMute;
    StopAudio;
  end;

  Result := True;
end;

function TAHMVisualisationRenderer.Mute: Boolean;
begin
  // Set default result - couldn't mute
  Result := False;

  with DirectShowInstance do
    if Assigned(DirectShowAudioRenderer) then
      Result := DirectShowAudioRenderer.InternalMute;
end;

function TAHMVisualisationRenderer.UnMute: Boolean;
begin
  // Set default result - couldn't unmute
  Result := False;

  with DirectShowInstance do
    if Assigned(DirectShowAudioRenderer) then
      Result := DirectShowAudioRenderer.InternalUnMute;
end;

function TAHMVisualisationRenderer.FastForward: Boolean;
var
  Position: LongInt;
begin
  // Set default result - can't fast forward
  Result := False;

  // Okay to update media position?
  if not OkayToPositionMedia then
  begin
    // We recently updated media position, so give DirectShow a chance to
    // catch up before we make another request. However, we'll return a true
    // result to prevent media manager from delegating the request to the
    // other media stream.
    Result := True;
    Exit;
  end;

  with DirectShowInstance do
    if Assigned(DirectShowAudioRenderer) then
    begin
      Position := DirectShowAudioRenderer.Position;
      Position := Position + DEFAULT_FWD_SKIP;
      if Position > DirectShowAudioRenderer.Duration then
        Position := DirectShowAudioRenderer.Duration;
      Result := DirectShowAudioRenderer.Seek(Position);
    end;
end;

function TAHMVisualisationRenderer.Rewind: Boolean;
var
  Position: LongInt;
begin
  // Set default result - can't fast forward
  Result := False;

  // Okay to update media position?
  if not OkayToPositionMedia then
  begin
    // We recently updated media position, so give DirectShow a chance to
    // catch up before we make another request. However, we'll return a true
    // result to prevent media manager from delegating the request to the
    // other media stream.
    Result := True;
    Exit;
  end;

  with DirectShowInstance do
    if Assigned(DirectShowAudioRenderer) then
    begin
      Position := DirectShowAudioRenderer.Position;
      Position := Position - DEFAULT_BWD_SKIP;
      if Position < 0 then Position := 0;
      Result := DirectShowAudioRenderer.Seek(Position);
    end;
end;

function TAHMVisualisationRenderer.SkipForward: Boolean;
begin
  // Stop current file & play new item
  Stop;
  Result := Play;
end;

function TAHMVisualisationRenderer.SkipBackward: Boolean;
begin
  // Stop current file & play new item
  Stop;
  Result := Play;
end;

end.
